---@class JetSpawner
JetSpawner = {}

local version = {
    MAJOR = 2,
    MINOR = 0,
    PATCH = 2,
    Special = 'beta'
}

do
--- 参数配置
    local DebugMode = false -- Debug模式

    local ONLY_COUNT_AIRBORNE_PLAYERS = true -- 是否仅计算已升空玩家
    local PLAYER_AIRBORNE_AGL = 100*0.305 --m 大约100ft, 玩家超过这个高度视为已升空
    
    -- 预设飞机配置
    local jetPresets = {
        [coalition.side.RED] = {
            { 
            categoryName = 'R1', -- 飞机类型预设, 建议用能读懂的名字来分类, 方便自己后续管理.
            groups = {'1-1','1-2','1-3','1-4'}, -- 预设的群组名, 每次刷新随机抽取, 建议用能读懂的名字来分类, 方便自己后续管理.
            baseSpawnTime = 1041, -- 基础刷新时间
            maxSpawn = 2, -- 最大数量限制, 设置为 >0 的数视为启用, 设置为 <=0 的数或者 nil/false 视为不启用.
            zones = {},-- 区域限制, 设置为空表, 或者直接删除/nil, 视为不启用.
            hidden = false -- 是否在F10地图不可见(疑似MP服务器有bug不能正常生效). 设置为 <=0 的数或者 nil/false 视为不启用.
            },
            { 
            categoryName = 'R2',
            groups = {'2-1','2-2','2-3','2-4','2-5','2-6'},
            baseSpawnTime = 1210,
            maxSpawn = 2,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'R3',
            groups = {'3-1','3-2','3-3','3-4','3-6'},
            baseSpawnTime = 1062,
            maxSpawn = 2,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'R4',
            groups = {'4-1','4-2','4-3','4-4'},
            baseSpawnTime = 1091,
            maxSpawn = 2,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'R5',
            groups = {'5-1','5-2','5-3','5-4','5-6'},
            baseSpawnTime = 1350,
            maxSpawn = 2,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'R6',
            groups = {'6-1','6-2','6-3','6-4','6-6'},
            baseSpawnTime = 1870,
            maxSpawn = 2,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'R7',
            groups = {'4-5',"4-6","4-7","4-8",'4-9'},
            baseSpawnTime = 1247,
            maxSpawn = 2,
            zones = {},
            hidden = true
            },
            { 
            categoryName = 'R8',
            groups = {'3-5','1-5','5-5','6-5'},
            baseSpawnTime = 1451,
            maxSpawn = 2,
            zones = {},
            hidden = true
            },
            { 
            categoryName = 'EW',
            groups = {'EW1','EW2','EW3'},
            baseSpawnTime = 1900,
            maxSpawn = 5,
            zones = {},
            hidden = false
            },
            { 
            categoryName = 'RG1阿纳帕', 
            groups = {'A-1','A-2',"A-3","A-4"}, 
            baseSpawnTime = 951, 
            maxSpawn = 2, 
            zones = {'阿纳帕'},
            hidden = false
            },
            { 
            categoryName = 'RG2克雷姆斯克', 
            groups = {'B-1','B-2',"B-3","B-4"}, 
            baseSpawnTime = 1051, 
            maxSpawn = 2, 
            zones = {'克雷姆斯克'},
            hidden = false
            },
            { 
            categoryName = 'RG3中心区', 
            groups = {'C-1','C-2',"C-3","C-4"}, 
            baseSpawnTime = 1303, 
            maxSpawn = 1, 
            zones = {'中心区'},
            hidden = false
            },
            { 
            categoryName = 'RG4克拉斯诺达尔', 
            groups = {'D-1','D-2',"D-3","D-4"},
            baseSpawnTime = 1121, 
            maxSpawn = 2,
            zones = {'克拉斯诺达尔'},
            hidden = false
            },
            { 
            categoryName = 'RG5迈科普', 
            groups = {'E-1','E-2',"E-3","E-4"}, 
            baseSpawnTime = 1463, 
            maxSpawn = 3, 
            zones = {'迈科普'},
            hidden = false
            },
            { 
            categoryName = 'RG6索契', 
            groups = {'F-1','F-2',"F-3","F-4"}, 
            baseSpawnTime = 1478, 
            maxSpawn = 1, 
            zones = {'索契'},
            hidden = false
            },
            { 
            categoryName = 'RG7古达乌塔', 
            groups = {'G-1','G-2',"G-3","G-4"}, 
            baseSpawnTime = 1401, 
            maxSpawn = 2, 
            zones = {'古达乌塔'},
            hidden = false
            },
            { 
            categoryName = 'RG8苏呼米', 
            groups = {'H-1','H-2'}, 
            baseSpawnTime = 1642, 
            maxSpawn = 1, 
            zones = {'苏呼米'},
            hidden = false
            },
            { 
            categoryName = 'RG9塞纳基', 
            groups = {'I-1'}, 
            baseSpawnTime = 2242, 
            maxSpawn = 3,
            zones = {'塞纳基'},
            hidden = false
            },
            { 
            categoryName = 'RG10库塔伊西',
            groups = {'J-1'}, 
            baseSpawnTime = 2042, 
            maxSpawn = 3, 
            zones = {'库塔伊西'},
            hidden = false
            },
            { 
            categoryName = 'RG11科布列季', 
            groups = {'K-1','K-2'}, 
            baseSpawnTime = 1631, 
            maxSpawn = 2, 
            zones = {'科布列季'},
            hidden = false
            },
            { 
            categoryName = 'RG12巴统', 
            groups = {'L-1'},
            baseSpawnTime = 1052, 
            maxSpawn = 1,
            zones = {'巴统'},
            hidden = false
            },
            { 
            categoryName = 'RG13矿水城', 
            groups = {'M-1','M-2','M-3'},
            baseSpawnTime = 1749,
            maxSpawn = 2,
            zones = {'矿水城'},
            hidden = false
            },
            { 
            categoryName = 'RG14纳尔奇克', 
            groups = {'N-1','N-2'}, 
            baseSpawnTime = 1845, 
            maxSpawn = 2, 
            zones = {'纳尔奇克'},
            hidden = false
            },
            { 
            categoryName = 'RG15莫兹多克', 
            groups = {'O-1','O-2'},
            baseSpawnTime = 1745, 
            maxSpawn = 2,
            zones = {'莫兹多克'},
            hidden = false
            },
            { 
            categoryName = 'RG16别斯兰', 
            groups = {'P-1'}, 
            baseSpawnTime = 2145, 
            maxSpawn = 2, 
            zones = {'别斯兰'},
            hidden = false
           },
        },
        [coalition.side.BLUE] = {
            { 
            categoryName = 'BG1阿纳帕', 
            groups = {'BA-1','BA-2',"BA-3","BA-4"}, 
            baseSpawnTime = 1111, 
            maxSpawn = 1, 
            zones = {'阿纳帕'},
            hidden = false
            },
            { 
            categoryName = 'BG2克雷姆斯克', 
            groups = {'BB-1','BB-2',"BB-3","BB-4"}, 
            baseSpawnTime = 1022, 
            maxSpawn = 1, 
            zones = {'克雷姆斯克'},
            hidden = false
            },
            { 
            categoryName = 'BG3中心区', 
            groups = {'BC-1','BC-2',"BC-3","BC-4"}, 
            baseSpawnTime = 1688, 
            maxSpawn = 1, 
            zones = {'中心区'},
            hidden = false
            },
            --{ 
            --categoryName = 'BG4克拉斯诺达尔', 
            --groups = {'BD-1','BD-2',"BD-3","BD-4"},
            --baseSpawnTime = 1321, 
            --maxSpawn = 1,
            --zones = {'克拉斯诺达尔'},
            --hidden = false
            --},
            { 
            categoryName = 'BG5迈科普', 
            groups = {'BE-1','BE-2',"BE-3","BE-4"}, 
            baseSpawnTime = 1766, 
            maxSpawn = 1, 
            zones = {'迈科普'},
            hidden = false
            },
            { 
            categoryName = 'BG6索契', 
            groups = {'BF-1','BF-2',"BF-3","BF-4"}, 
            baseSpawnTime = 1999, 
            maxSpawn = 1, 
            zones = {'索契'},
            hidden = false
            },
            { 
            categoryName = 'BG7古达乌塔', 
            groups = {'BG-1','BG-2',"BG-3","BG-4"}, 
            baseSpawnTime = 1201, 
            maxSpawn = 1, 
            zones = {'古达乌塔'},
            hidden = false
            },
            { 
            categoryName = 'BG8苏呼米', 
            groups = {'BH-1','BH-2'}, 
            baseSpawnTime = 2042, 
            maxSpawn = 1, 
            zones = {'苏呼米'},
            hidden = false
            },
            { 
            categoryName = 'BG9塞纳基', 
            groups = {'BI-1'}, 
            baseSpawnTime = 2720, 
            maxSpawn = 1,
            zones = {'塞纳基'},
            hidden = false
            },
            { 
            categoryName = 'BG10库塔伊西',
            groups = {'BJ-1'}, 
            baseSpawnTime = 2942, 
            maxSpawn = 1, 
            zones = {'库塔伊西'},
            hidden = false
            },
            { 
            categoryName = 'BG11科布列季', 
            groups = {'BK-1','BK-2'}, 
            baseSpawnTime = 2501, 
            maxSpawn = 1, 
            zones = {'科布列季'},
            hidden = false
            },
            { 
            categoryName = 'BG12巴统', 
            groups = {'BL-1'},
            baseSpawnTime = 1800, 
            maxSpawn = 1,
            zones = {'巴统'},
            hidden = false
            },
            { 
            categoryName = 'BG13矿水城', 
            groups = {'BM-1','BM-2','BM-3'},
            baseSpawnTime = 1649,
            maxSpawn = 1,
            zones = {'矿水城'},
            hidden = false
            },
            { 
            categoryName = 'BG14纳尔奇克', 
            groups = {'BN-1','BN-2'}, 
            baseSpawnTime = 1645, 
            maxSpawn = 1, 
            zones = {'纳尔奇克'},
            hidden = false
            },
            { 
            categoryName = 'BG15莫兹多克', 
            groups = {'BO-1','BO-2'},
            baseSpawnTime = 3011, 
            maxSpawn = 1,
            zones = {'莫兹多克'},
            hidden = false
            },
            { 
            categoryName = 'BG16别斯兰', 
            groups = {'BP-1'}, 
            baseSpawnTime = 2851, 
            maxSpawn = 1, 
            zones = {'别斯兰'},
            hidden = false
           },
        },
    }

    local MonitorRepeatTime = 1 -- Seconds 回调函数更新频率
    local DEFAULT_BASE_SPAWN_TIME = 60 --Second, 默认基础刷新时间

    -- 设置玩家所在阵营
    local plyerSide = {
        [coalition.side.RED] = false,
        [coalition.side.BLUE] = true,
    }

    -- 根据玩家调整刷新率
    local Player_Ratio_Table = {
        [3] = 2.5,      -- 当玩家数小于3人刷新率为 2.5
        [5] = 2,
        [8] = 1.5,
        [10] = 1,
        [15] = 0.8,  
        [20] = 0.75,
        [25] = 0.65,
        [30] = 0.55,
        [35] = 0.45,
        [40] = 0.35,
        [45] = 0.25,
        [50] = 0.15,
        [60] = 0.1,
        [9999] = 0.05,
    }

    local Player_Ratio_Table_PlayerSide = {
        [3] = 0.65,
        [5] = 0.75,
        [8] = 0.85,
        [10] = 0.9,
        [15] = 1,         
        [20] = 1.2,
        [25] = 1.3,
        [30] = 1.5,
        [35] = 1.7,
        [40] = 2.5,
        [45] = 3.5,
        [50] = 4.5,
        [55] = 5.5,
        [60] = 6.5,
        [9999] = 7.5,
    }

    local RANDOM_DEVIATION = 0.75  -- 时间随机浮动比例(75%)

    -- 玩家对立阵营
    local otherSide = {
        [coalition.side.BLUE] = coalition.side.RED,
        [coalition.side.RED] = coalition.side.BLUE,
    }

    -- 音频配置
    local PlaySound = false -- 音频开关
    local SoundFilePathEnemy = 'Sound/enemy/'
    local SoundFilePathPlayer = 'Sound/ally/'
    local EnemySpawnSFX = {
        'enemy_1',
        'enemy_2',
        'enemy_3',
        'enemy_4',
        'enemy_5',
    }
    local AllyKilledSFX = {
        'ally_1',
        'ally_2',
        'ally_3',
        'ally_4',
        'ally_5',
        'ally_6',
        'ally_7',
        'ally_8',
        'ally_9',
        'ally_10',
    }
--- DCS 脚本体操
    --- Get Group.
    ---@param groupIdent string|Group Can be group name or DCS.Group.
    ---@return Group|nil
    local function getGroup(groupIdent)
        if not groupIdent then return end

        local group = groupIdent

        if type(groupIdent)=='string' then
            group = Group.getByName(groupIdent)
        end

        if not group or not group.isExist or not group:isExist() then return end
        if not group.getUnit or not group.getUnits then return end
        if not group.getController then return end
        if not group.getID then return end
        if not group.getName then return end
        if not group.getCoalition then return end

        return group
    end
    
    --- Get Unit.
    ---@param DCSunit Unit|string a DCS.Unit object or its name.
    ---@param alive boolean if true will check if this unit is alive.
    ---@param player boolean if true will check if this unit controlled by player.
    ---@return Unit|nil
    local function getUnit(DCSunit,alive,player)
        local unit = DCSunit
        if type(DCSunit)=='string' then
            unit = Unit.getByName(DCSunit)
        end

        if not unit then return end
        if not unit.getGroup then return end
        if not unit.getTypeName then return end
        if not unit.getID then return end
        if not unit.getCoalition then return end
        if not unit.getController then return end

        if not unit.isExist or not unit:isExist() then return end

        if alive then
            if not unit.getLife or unit:getLife()<1 then return end
        end

        if player then
            if not unit.getPlayerName or not unit:getPlayerName() then return end
        end

        return unit
    end

    --- Get unit(1).
    ---@param DCSgroup Group a DCS.Group object.
    ---@param alive boolean if true will check if this unit is alive.
    ---@param player boolean if true will check if this unit controlled by player.
    ---@return Unit|nil
    local function getFirstUnit(DCSgroup,alive,player)
        local group = getGroup(DCSgroup)
        if not group then return end

        local unit = getUnit(group:getUnit(1),alive,player)
        if not unit then return end

        return unit
    end

    --- Get Unit's group.
    ---@param DCSunit Unit|string a DCS.Unit object or its name.
    ---@return Group|nil
    local function getUnitGroup(DCSunit,alive,player)
        local unit = getUnit(DCSunit,alive,player)
        if not unit then return end

        return getGroup(unit:getGroup())
    end

--- 私有函数
    --- 工具
    -- 获取表大小
    local function getTableSize(table)
        if not table then return end

        local tableSize = 0
        for __ in pairs(table) do
            tableSize = tableSize + 1
        end

        return tableSize
    end

    -- 输出表内容
    ---@param table table to show. not a complex table though.
    ---@param level number level of this sub table.
    ---@return string formated result.
    local function tableShow(table, level)
        if type(table) ~= 'table' then return tostring(table) end
        if getTableSize(table) <= 0 then return '{}' end

        level = level or 1
        local pushBack = '  '

        local result = '{\n'
        for index,value in pairs(table) do
            for i=1,level,1 do
                result = result..pushBack
            end
            result = result..'[\"'..index..'\"] = '

            if type(value) == 'table' then
                result = result..tableShow(value,level+1)..',\n'
            end

            if type(value) == 'string' then
                result = result..'\"'..value..'\",\n'
            end

            if type(value) == 'number' then
                result = result..value..',\n'
            end

            if type(value) == 'function' then
                result = result..'[function] '..value..',\n'
            end

            if type(value) == 'boolean' then
                result = result..tostring(value)..',\n'
            end
        end

        if level > 1 then
            for i=2,level,1 do
                result = result..pushBack
            end
        end

        return result..'}'
    end

    --- 深度拷贝一个[table]类型数据
    local function deepCopy(object)
		local lookup_table = {}
		local function _copy(object)
			if type(object) ~= "table" then
				return object
			elseif lookup_table[object] then
				return lookup_table[object]
			end
			local new_table = {}
			lookup_table[object] = new_table
			for index, value in pairs(object) do
				new_table[_copy(index)] = _copy(value)
			end
			return setmetatable(new_table, getmetatable(object))
		end
		return _copy(object)
	end

    -- 输出调试信息
    ---@param arg string|table the debug message.
    ---@param Delay number message show delay.
    local function Debug(message,Delay,forceShow,title)
        if not (DebugMode or forceShow) then return end

        if not message then message = 'false or nil.' end
        Delay = Delay or 10

        local title = title or '[JetSpawner Debug] '

        if type(message)=='table' then 
            message = tableShow(message)
        end
        if type(message)~='string' then message = tostring(message) end

        message = title..message
        trigger.action.outText(message,Delay)
        env.info(message)
    end


--- 私有参数
    -- 表
    local AllJetSpawners = {
        [coalition.side.RED] = {},
        [coalition.side.BLUE] = {},
    }

    local PlayerList = {
        [coalition.side.RED] = {size = 0},
        [coalition.side.BLUE] = {size = 0},
        addPlayer = function(self,playerGroupName,tCoalition,event)
            if not self[tCoalition][playerGroupName] then
                self[tCoalition][playerGroupName] = true
                self[tCoalition].size = self[tCoalition].size + 1
            end

            --Debug
            if not event then return end
            local eventTable = {
                [world.event.S_EVENT_BIRTH] = 'S_EVENT_BIRTH',
                [world.event.S_EVENT_TAKEOFF] = 'S_EVENT_TAKEOFF',
            }
            Debug("Player number +1 by \'event "..eventTable[event.id].." handler.\' Player group name: "..playerGroupName..' current playerNum: '..self[tCoalition].size)
        end,
        removePlayer = function(self,playerGroupName,tCoalition,event)
            if self[tCoalition][playerGroupName] then
                self[tCoalition][playerGroupName] = nil
                self[tCoalition].size = self[tCoalition].size - 1
                if self[tCoalition].size < 0 then
                    self[tCoalition].size = 0
                end
            end

            --播放音频
            JetSpawner.playAllyKilledSound({coalition = tCoalition})

            --Debug
            if not event then return end
            local eventTable = {
                [world.event.S_EVENT_CRASH] = 'S_EVENT_CRASH',
                [world.event.S_EVENT_LAND] = 'S_EVENT_LAND',
                [world.event.S_EVENT_EMERGENCY_LANDING] = 'S_EVENT_EMERGENCY_LANDING',
                [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = 'S_EVENT_PLAYER_LEAVE_UNIT',
            }
            Debug("Player number -1 by \'event "..eventTable[event.id].." handler.\' Player group name: "..playerGroupName..' current playerNum: '..self[tCoalition].size)
        end,
    }
    
    -- 事件监听
    local eventHandler = {}
    function eventHandler:onEvent(event)
        --- 为 PlayerList 添加 玩家
        if (ONLY_COUNT_AIRBORNE_PLAYERS and event.id == world.event.S_EVENT_TAKEOFF) or (not ONLY_COUNT_AIRBORNE_PLAYERS and event.id == world.event.S_EVENT_BIRTH) then
            local unit = getUnit(event.initiator, true, true)
            if not unit then return end
            
            local playerGroup = getUnitGroup(unit)
            if not playerGroup then return end
            local playerGroupName = playerGroup:getName()

            local tCoalition = unit:getCoalition()
            
            if playerGroupName then
                PlayerList:addPlayer(playerGroupName, tCoalition, event)
            end
        end
        
        --- 为 PlayerList 移除 玩家
        if event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_EMERGENCY_LANDING or event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT then
            local unit = getUnit(event.initiator,nil,true)
            if not unit then return end

            local playerGroup = getUnitGroup(unit)
            if not playerGroup then return end
            local playerGroupName = playerGroup:getName()

            local tCoalition = unit:getCoalition()
            if playerGroupName then
                PlayerList:removePlayer(playerGroupName,tCoalition,event)
            end
        end
    end
    world.addEventHandler(eventHandler)


--- 调试工具
    env.setErrorMessageBoxEnabled(DebugMode)
    if DebugMode then
        jetPresets = {
            [coalition.side.BLUE] = {
                { 
                    categoryName = 'CAS',
                    groups = {'Blue_Aerial-1','Blue_Aerial-2'},
                    baseSpawnTime = 1,
                    maxSpawn = 2,
                    zones = {'zone-1'},
                },
                { 
                    categoryName = 'CAP', 
                    groups = {'Blue_Aerial-3','Blue_Aerial-4','Blue_Aerial-5'}, 
                    baseSpawnTime = 5, 
                    maxSpawn = 3, 
                },
                { 
                    categoryName = 'AWACS', 
                    groups = {'Blue_Aerial-6','Blue_Aerial-7'}, 
                    baseSpawnTime = 10, 
                    maxSpawn = 1, 
                },
            },
            [coalition.side.RED] = {
                { 
                    categoryName = 'CAS',
                    groups = {'Red_Aerial-1'},
                    baseSpawnTime = 1,
                    maxSpawn = 1,
                    zones = {'zone-1'},
                },
                { 
                    categoryName = 'CAP',
                    groups = {'Red_Aerial-2','Red_Aerial-3','Red_Aerial-4','Red_Aerial-5',},
                    baseSpawnTime = 5,
                    maxSpawn = 4,
                },
                { 
                    categoryName = 'AWACS', 
                    groups = {'Red_Aerial-6','Red_Aerial-7',}, 
                    baseSpawnTime = 10, 
                    maxSpawn = 2, 
                },
            },
        }

        -- Debug(jetPresets,1,nil,'jetPresets ')
    end

--- 公有函数
    --- New()
    ---@param params table
    ---@param params.coalition number coalition.side
    ---@param params.categoryName string category of this spawner.
    ---@param params.baseSpawnTime number|nil base spawn time for this template group, nil for defalut.
    ---@param params.maxSpawn number|nil max spawn number for this template group, nil for no limit.
    ---@param params.zones table|nil template group spawn limit zone(s), empty table "{}" or nil for no limit.
    ---@param params.hidden boolean if this group can be seen on F10 map after spawn, nil|false for not be seen.
    ---@return JetSpawner
    function JetSpawner:New(params)
        if type(params)~='table' then return end

        if not params.coalition then return end
        if not params.categoryName then return end

        params.coalition = tonumber(params.coalition)
        if params.coalition ~= coalition.side.RED and params.coalition ~= coalition.side.BLUE then return end
        
        if AllJetSpawners[params.coalition][params.categoryName] then
            AllJetSpawners[params.coalition][params.categoryName]:Remove()
        end

        local obj = {}
        obj.categoryName = params.categoryName
        obj.coalition = params.coalition

        obj.baseSpawnTime = params.baseSpawnTime or DEFAULT_BASE_SPAWN_TIME
        obj.maxSpawn = params.maxSpawn
        obj.hidden = params.hidden

        obj.zones = params.zones

        obj.templates = {}
        obj.templateGroupNames = {}
        obj.spawnedGroups = {}

        setmetatable(obj, self)
        self.__index = self

        obj:Start(obj.baseSpawnTime)

        AllJetSpawners[params.coalition][params.categoryName] = obj
        return obj
    end

    --- 移除一个类型的JetSpawner
    function JetSpawner:Remove()
        self:Stop()

        if AllJetSpawners[self.coalition][self.categoryName] then
            AllJetSpawners[self.coalition][self.categoryName] = nil
        end
    end

    --- 添加预设
    ---@param groupName string name of a DCS.Group name for template.
    ---@return JetSpawner
    function JetSpawner:addTemplate(groupName)
        if not groupName or type(groupName)~='string' then return self end

        -- 检查是否重复添加
        for i,templateName in ipairs(self.templateGroupNames) do
            if templateName == groupName then
                Debug('group: '..groupName..' has already been added as a template.')
                return self
            end
        end

        local sideName = {
            [coalition.side.RED] = 'red',
            [coalition.side.BLUE] = 'blue',
        }

        local missionJets = env.mission.coalition[sideName[self.coalition]].country
        if not missionJets then return end

        for i,countryData in ipairs(missionJets) do
            for j, groupData in ipairs(countryData.plane.group) do
                if groupData.name == groupName then
                    local tempData = deepCopy(groupData)
                    tempData.lateActivation = false
                    -- 直接以群组名为键存储
                    self.templates[groupName] = tempData
                    table.insert(self.templateGroupNames,groupName)
                    return self
                end
            end
        end

        -- 如果在mission文件夹没找到对应预设组
        Debug("Template group not found: " .. groupName)

        return self
    end

    --- 删除预设
    ---@param groupName string name of a DCS.Group name for template.
    ---@return JetSpawner
    function JetSpawner:removeTemplate(groupName)
        if not groupName or type(groupName)~='string' then return self end
        if not self.templates[groupName] then return self end

        self.templates[groupName] = nil

        for i,templateName in ipairs(self.templateGroupNames) do
            if templateName == groupName then
                table.remove(self.templateGroupNames,i)
            end
        end

        return self
    end

    --- 修改基础刷新时间
    ---@param newTime number|nil of a new base spawn time for this category, nil for default.
    ---@return JetSpawner
    function JetSpawner:setBaseSpawnTime(newTime)
        self.baseSpawnTime = newTime or DEFAULT_BASE_SPAWN_TIME

        return self
    end

    --- 添加绑定区域
    ---@param newZoneName string of a new zone for this category.
    ---@return JetSpawner
    function JetSpawner:addZone(newZoneName)
        if not newZoneName or type(newZoneName)~='string' then return self end
        
        if not self.zones then
            self.zones = {newZoneName}
            return self
        end

        for i,zoneName in ipairs(self.zones) do
            if zoneName == newZoneName then return self end
        end
        table.insert(self.zones,newZoneName)

        return self
    end

    --- 删除绑定区域
    ---@param targetZoneName string of the target zone for this category to remove.
    ---@return JetSpawner
    function JetSpawner:removeZone(targetZoneName)
        if not targetZoneName or type(targetZoneName)~='string' then return self end
        if not self.zones then return end

        for i,zoneName in ipairs(self.zones) do
            if zoneName == targetZoneName then
                table.remove(self.zones,i)
                return self 
            end
        end

        return self 
    end

    --- 设置最大数量
    ---@param number number of new max spawn number, nil for no limit.
    ---@return JetSpawner
    function JetSpawner:setMaxSpawn(number)
        if not number then return self end
        number = tonumber(number)

        self.maxSpawn = number
        
        return self
    end

    --- 是否达到数量限制
    ---@return boolean if this spawner can spawn under the max spawn number limit, true for can spawn.
    function JetSpawner:checkMaxNumber()
        if not self.maxSpawn or self.maxSpawn <= 0 then return true end

        local spawnedNumber = 0
        for category_Type_Name, groups in pairs(self.spawnedGroups) do
            spawnedNumber = spawnedNumber + getTableSize(groups)
        end

        return spawnedNumber < self.maxSpawn
    end

    --- 是否存在区域限制
    ---@return boolean if this spawner can spawn under the limit of zones, true for can spawn.
    function JetSpawner:checkZone()
        if not self.zones or getTableSize(self.zones) < 1 then return true end

        local haveEnemyInZone = false
        local haveAllyInZone = false

        for i,zoneName in ipairs(self.zones) do
            if type(zoneName) == 'string' then
                local zone = trigger.misc.getZone(zoneName)
                if zone then
                    local sphere = {
                        id = world.VolumeType.SPHERE,
                        params = zone
                    }
                    world.searchObjects({Object.Category.UNIT},sphere,function (foundItem,vals)
                        if foundItem.isActive and foundItem:isActive() then
                            if foundItem.getLife and foundItem:getLife() >= 1 then
                                if foundItem:getDesc().category == Unit.Category.GROUND_UNIT then
                                    Debug('foundItem category: '..foundItem:getDesc().category)
                                    if foundItem.getCoalition and foundItem:getCoalition() == self.coalition then
                                        haveAllyInZone = true
                                    end

                                    if foundItem.getCoalition and foundItem:getCoalition() == otherSide[self.coalition] then
                                        haveEnemyInZone = true
                                    end
                                end
                            end
                        end
                    end)
                end
            end
        end

        return haveAllyInZone and (not haveEnemyInZone)
    end

    --- 刷新飞机
    function JetSpawner:spawnJet(templateName)
        if not templateName then return end

        templateName = tostring(templateName)

        local spawnData = self.templates[templateName]
        if not spawnData then 
            Debug('spawnJet - not found template data for group: '..templateName)
            return
        end

        local presetGroup = Group.getByName(templateName)
        if not presetGroup then
            Debug('spawnJet - not found template group: '..templateName)
            return
        end

        local coalitionNames = {
            [coalition.side.RED] = 'Red_',
            [coalition.side.BLUE] = 'Blue_',
        }

        spawnData.name = nil
        local newGroupNamePreset = coalitionNames[self.coalition]..self.categoryName..'_'..spawnData.units[1].type

        -- 初始化 spawnedGroups
        self.spawnedGroups[newGroupNamePreset] = self.spawnedGroups[newGroupNamePreset] or {}

        -- 如果是第一次刷新这类型预设
        if getTableSize(self.spawnedGroups[newGroupNamePreset]) < 1 then
            spawnData.name = newGroupNamePreset..'-1'
        end

        -- 如果不是第一次刷新这类型预设
        if getTableSize(self.spawnedGroups[newGroupNamePreset]) >= 1 then
            -- 检查之前是否有这个名字的群组刷新过, 且已经阵亡
            for i, groupName in ipairs(self.spawnedGroups[newGroupNamePreset]) do
                if not getGroup(groupName) then
                    spawnData.name = groupName
                end
            end

            -- 如果之前刷新的同类飞机都存在, 那就增加一个新的
            if not spawnData.name then
                spawnData.name = newGroupNamePreset..'-'..getTableSize(self.spawnedGroups[newGroupNamePreset])+1
            end
        end

        -- 设置是否F10可见
        spawnData.hidden = self.hidden or false

        -- 生成单位
        local countrys = {
            [coalition.side.RED] = country.id.CJTF_RED,
            [coalition.side.BLUE] = country.id.CJTF_BLUE,
        }

        local category = presetGroup:getCategory()
        for i,unitData in ipairs(spawnData.units) do
            unitData.name = spawnData.name..'-'..i
        end


        local newGroup = coalition.addGroup(countrys[self.coalition],category,spawnData)
        if newGroup then
            Debug('New group spawn: '..newGroup:getName())
            table.insert(self.spawnedGroups[newGroupNamePreset],newGroup:getName())
            return newGroup
        end
    end

    --- 设定下次刷新时间
    function JetSpawner:getNextSpawnTime()
        local Ratio = 0.05
        local RatioTable = Player_Ratio_Table_PlayerSide
        local sideNum = self.coalition

        if not plyerSide[self.coalition] then
            RatioTable = Player_Ratio_Table
            sideNum = otherSide[self.coalition]
        end

        local keys = {}
        for key in pairs(RatioTable) do
            table.insert(keys, key)
        end
        -- 对键进行排序
        table.sort(keys)

        local ratioLevel = 999
        for _,maxPlayer in pairs(keys) do
            if PlayerList[sideNum].size <= maxPlayer then
                Ratio = RatioTable[maxPlayer]
                break
            end
        end

        local nextSpawnTime = self.baseSpawnTime
        local minTime = math.max(0, nextSpawnTime * (1 - RANDOM_DEVIATION))
        local maxTime = nextSpawnTime * (1 + RANDOM_DEVIATION)
        nextSpawnTime = math.random(minTime, maxTime)
        Debug('getNextSpawnTime - Coalition: '..self.coalition..' playerNumber:' ..PlayerList[sideNum].size..' ratio: '..Ratio..'[PlayerSide: '..tostring(plyerSide[self.coalition])..'] Preset: '..self.categoryName..' nextSpawnTime: '..nextSpawnTime * Ratio)
        
        return nextSpawnTime * Ratio
    end

    --- 播放刷新音频
    function JetSpawner:playSpawnSound()

        if PlaySound then
            local soundfile = SoundFilePathEnemy..EnemySpawnSFX[math.random(1,#EnemySpawnSFX)]..'.ogg'
            trigger.action.outSoundForCoalition(otherSide[self.coalition],soundfile)
            Debug('PlaySound - Enemy soundfile: '..soundfile)
        end

        return self
    end

    --- 播放友军阵亡音频
    function JetSpawner:playAllyKilledSound()

        if PlaySound then
            local soundfile = SoundFilePathPlayer..AllyKilledSFX[math.random(1,#AllyKilledSFX)]..'.ogg'
            -- trigger.action.outSound(soundfile)
            trigger.action.outSoundForCoalition(self.coalition,soundfile)
            Debug('PlaySound - Player soundfile: '..soundfile)
        end

        return self
    end

--- 回调函数
    function JetSpawner._MonitorFunction(vars,time)
        local self = vars.context
        local repeatTime = vars.repeatTime

        --- 检查已经刷新的飞机状态
        for category_Type_Name, groups in pairs(self.spawnedGroups) do
            for i, groupName in ipairs(groups) do
                -- 移除不存在的群组
                local tempGroup = getGroup(groupName)
                if not tempGroup then
                    table.remove(groups,i)
                    Debug('群组已不存在, 移除群组: '..groupName)
                end

                if tempGroup then
                    if not getFirstUnit(tempGroup,true) then
                        table.remove(groups,i)
                        Debug('群组已不存在, 移除群组: '..groupName)
                    end
                end
            end
        end
        
        --- 抽取要刷新的飞机模板
        local templateNames = self.templateGroupNames
        local templateGroupName = templateNames[math.random(1,#templateNames)]
        Debug('刷新群组: '..templateGroupName)

        --- 判断刷新条件
        -- 是否达到数量限制
        local notNumberLimit = self:checkMaxNumber()
        Debug('刷新条件 - 数量限制:'..tostring(notNumberLimit))

        -- 是否存在区域限制
        local notZoneLimit = self:checkZone()
        Debug('刷新条件 - 区域限制:'..tostring(notZoneLimit))

        local canSpawn = notNumberLimit and notZoneLimit
        --- 刷新飞机
        if canSpawn then
            local newGroup = self:spawnJet(templateGroupName)
            if newGroup then
                -- 播放音频
                if PlaySound then
                    self:playSpawnSound()
                end
            end
        end

        --- 设定下次刷新时间
        repeatTime = self:getNextSpawnTime()

        return time + repeatTime
    end

    function JetSpawner:Start(Delay,RepeatScanSeconds)
        Debug('JetSpawner:Start')
        local tDelay = Delay or 1
        local RepeatScanInterval = RepeatScanSeconds or MonitorRepeatTime

        if self.MonitorID then
            self:Stop()
        end

        self.MonitorID = timer.scheduleFunction(self._MonitorFunction,{context = self,repeatTime = RepeatScanInterval},timer.getTime() + tDelay)
    end
    
    function JetSpawner:Stop()
        Debug('JetSpawner:Stop')
        if not self.MonitorID then return end

        timer.removeFunction(self.MonitorID)
        self.MonitorID = nil
    end

    --- 初始化
    for tCoalition, preSets in pairs(jetPresets) do
        for _,preSet in ipairs(preSets) do
            local params = preSet
            params.coalition = tCoalition
            local spawner = JetSpawner:New(preSet)
            if spawner then
                for j,groupName in ipairs(preSet.groups) do
                    spawner:addTemplate(groupName)
                end
            end
        end
    end

end

trigger.action.outText('JetSpawner by Buta: version: '..version.MAJOR..'.'..version.MINOR..'.'..version.PATCH..version.Special,5)